<?
/*****************
datebook.php by Kyle Kienapfel 2006 (doctor.whom of gmail.com)

This script reads in a datebook.dat file from say Palm Desktop 4.1.4 and
outputs in iCalendar (RFC 2445). Output has been tested in Sunbird and 
Google Calendar.

Posted on Google Code or Google Project hosting.
http://code.google.com/p/palm2ical/

http://w3.misterhouse.net:81/mh/code/public/palm_calendar.pl (not by me) has a complete repeat system but doesn't convert to iCalendar, actually this seems to read the file in a way better way.
2006/9/16: actually its for the old format, similar method just probably different number of variables 
*************************/
//Configuration variables
//TZID (Los_Angeles), the America/Los_Angeles is cribbed from google calendar.
//Script assumes $TZID is set to something useful.
$TZID="America/Los_Angeles";
//the DST/ST timezone deltas
//File to open as a Palm Datebook, sent to file_get_contents
$Datebook="datebook.dat";

if ($_SERVER["PATH_INFO"] == "/demolay.ics") {
$IncludeCats = '$4$5$';
} elseif ($_SERVER["PATH_INFO"] == "/all.ics") {
$Birthdays=true;  // note that just a ; was here
} else {
$Birthdays=true;
$ExcludeCats = '$4$5$';
}

if (file_exists("config.txt")) {
  include_once("config.txt");
}

//Anything below this is not a good idea to mess up.
//Good to look at the actual code though.

//ord -- Return ASCII value of character
//list(,$c) = unpack("s","A\0");echo $c;

function xxdate($format, $t) {
//This funtion corrects the timezone to Pacific
//PDT is GMT-7 and PST is GMT-6
//This could be expanded to use a global variable, but PHP5 will make this unnecessary
//See: http://ca3.php.net/manual/en/function.date-default-timezone-set.php
if (date("I",$t)) {
// && date("Z",$t) != -25200
$Offset=date("Z",$t)-"-25200";
} else {
$Offset=date("Z",$t)-"-28800"; //whoops had as 21600
}
//print date("Z",$t)."\n";
return date($format,$t-$Offset);
}
function BitmaskWeekdays($number) {
//echo $number;
$MOTUWE = array("SU", "MO", "TU", "WE", "TH", "FR", "SA");
$bits = array(1,2,4,8,16,32,64,128);
//SU MO TU WE TH FR SA
// 1  2  4  8 16 32 64
for ($i=7;$i>=0;$i--) {
//echo "$number $i check for ".$bits[$i]."\n";

if ($bits[$i] <= $number) {
$number -= $bits[$i];
if (isset($WD)) {
$WD = ','.$WD;
}
$WD = $MOTUWE[$i].$WD;
}
}
return $WD;
} //func

function hexdebug($value) {
echo "$value\n";
echo chunk_split(implode(" ",unpack("H*",$value)),2," ") . "\n";
}

$DatebookFile = file_get_contents($Datebook);
$DatebookIndex=0;

$MOTUWE = array("SU", "MO", "TU", "WE", "TH", "FR", "SA");



//substr(string, start, length) 
//be ba fe ca could be an endianess detector.. CAFEBABE
list(,$Magic) = unpack("V",substr($DatebookFile,$DatebookIndex,4));
//$Magic=substr($DatebookFile,$DatebookIndex,4);
//3405691582 says calc, -889275714 says V
if ($Magic != -889275714) { echo "Doesn't look like a CAFEBABE file\n"; }
$DatebookIndex += 4;
//echo $DatebookIndex."<br>\n";
$DatebookIndex=40;
$DatebookEntries=ord(substr($DatebookFile,$DatebookIndex,1));
$DatebookIndex += 12;
//echo ord(substr($DatebookFile,$DatebookIndex,1));
//echo "There are $DatebookEntries entries<br>\n";

//This is where we start actually using the DatebookIndex to roll on forward
//StringLength to be a bitch variable like $i.
$StringLength = ord(substr($DatebookFile,$DatebookIndex,1));
$DatebookIndex += 1;
// don't really need to output the location of this file really
$DatebookLocation = substr($DatebookFile,$DatebookIndex,$StringLength);
$DatebookLocation = str_replace("\\","\\\\",$DatebookLocation);
$DatebookIndex += $StringLength;
//Moved up here so we can list categories
$Entries = explode("\xFF\xFF\xFF\x7F",$DatebookFile);
$Header=array_shift($Entries);

if ($_SERVER["PATH_INFO"] == "/categories" || $argv[1] == "categories") {
echo "<pre>Category List for the datebook.dat\n0 Unfiled\n";
$Cats["Unfiled"]=0;
//Tail 36 bytes with an event ID or something stupid
$Header = substr($Header,0,-36);
//It'd be a bitch to have to redo this and actually understand the format
$DatebookIndex += 47;
list(,$CategoryCount) = unpack("V",substr($Header,$DatebookIndex,4));
$DatebookIndex += 4;
for ($i = 1; $i <= $CategoryCount; $i++) {
list(,$catnum) = unpack("V",substr($Header,$DatebookIndex,4));
$DatebookIndex += 4;
list(,$catcolor) = unpack("V",substr($Header,$DatebookIndex,4));
$DatebookIndex += 4;
$DatebookIndex += 4;
$StringLength = ord(substr($Header,$DatebookIndex,1));
$DatebookIndex++;
$TitleA[$catnum]=substr($Header,$DatebookIndex,$StringLength);
$Title=substr($Header,$DatebookIndex,$StringLength);
$DatebookIndex += $StringLength;
$StringLength = ord(substr($Header,$DatebookIndex,1));
$DatebookIndex++;
$TitleB[$catnum]=substr($Header,$DatebookIndex,$StringLength);
$DatebookIndex += $StringLength;
//Incase we have to map back from the names to numbers incase they can be shuffled
$Cats[$Title]=$catnum;
echo $catnum." ".$TitleA[$catnum]."\n";
}
//echo chunk_split(implode(" ",unpack("H*",substr($Header,$DatebookIndex))),2," ");
//we'll use DatebookIndex to save having to advance on the file name again
//echo "\n$Header\n";
exit;
}
//echo substr($DatebookFile,$DatebookIndex,$StringLength);
//echo "<br>\n";
//Currently merely avoiding recursive links, but might expand on this, 
//for specifics to go through as text/calendar
// Intended audience is remote calendar apps
if (strstr($_SERVER["HTTP_USER_AGENT"],"Firefox") && ($_SERVER["PATH_INFO"] != "/datebook.ics")) { 
if (isset($_SERVER["PATH_INFO"])) {
print "<pre>Goes through as html on Firefox, force text/calendar with <a href=\"datebook.ics\">datebook.php/datebook.ics</a>\n\n";
} else {
print "<pre>Goes through as html on Firefox, force text/calendar with <a href=\"datebook.php/datebook.ics\">datebook.php/datebook.ics</a>\n\n";
}
}
else {
  header('Content-type: text/calendar');
}

?>
BEGIN:VCALENDAR
PRODID:-//Kyle Kienapfel/datebook.php//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Palm
X-WR-TIMEZONE:<?echo $TZID;?>

X-WR-CALDESC:Export of <?echo $DatebookLocation;?>

BEGIN:VTIMEZONE
TZID:America/Los_Angeles
X-LIC-LOCATION:America/Los_Angeles
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
TZNAME:PST
DTSTART:19701025T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
TZNAME:PDT
DTSTART:19700405T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
END:DAYLIGHT
END:VTIMEZONE
<? 
$Count=0;
//echo count($Entries);
foreach ($Entries as $value) {
$Count++;
$fullvalue=$value;
//It's either up here or right before the end of loop
unset($Note,$Location,$Exclusions,$xclusions);

$Category = ord(substr($value,4,1));
//echo "$IncludeCats\n";
if (isset($IncludeCats) && !strstr($IncludeCats,"\$$Category\$")) { continue; }
if (isset($ExcludeCats) && strstr($ExcludeCats,"\$$Category\$")) { continue; }
print "BEGIN:VEVENT\n";
//print "CATE_NUM:$Category\n";
list(,$StartTime) = unpack("V",substr($value,70,4)); 
list(,$EndTime) = unpack("V",substr($value,78,4)); 
$StringLength=ord(substr($value,90,1));
$Title = substr($value,91,$StringLength);
$Title = str_replace("\r\n","\\n",$Title);
print "SUMMARY:$Title\n";

//Cut the processed parts off...
$value = substr($value,91+$StringLength);

$StringLength = ord(substr($value,16,1)); $StringStart=17;
if ($StringLength == 255) { 
  list(,$StringLength) = unpack("v",substr($value,17,2)); 
  $StringStart=19; 
}
if ($StringLength != 0) { 
  $Note = substr($value,$StringStart,$StringLength); 
  $value = substr($value,$StringStart+$StringLength);
}
else { 
  $value = substr($value,17); 
}
//cut off some more common cruft
$value = substr($value,36);
//if not the last one, cut off last 36 bytes
//echo "Count $Count ".count($Entries)."\n";
if ($Count != count($Entries)) { 
//print "$tail\n";
//echo chunk_split(implode(" ",unpack("H*",$tail)),2," ") . "\n";
//echo chunk_split(implode(" ",unpack("H*",$fullvalue)),2," ") . "\n";
//$tail=substr($value,-36);
$value = substr($value,0,-36); 
}
//hexdebug($value);
// if md5($value) == "abc9b358f24c8c9b1a998e792d3eea3e" we're done
//current index 11 is place to check for location
if (substr($value,0,12) == "\0\0\0\0\x05\0\0\0\0\0\0\0") {
// No repeat information, move directly on to Location
$StringLength = ord(substr($value,12,1));
  if ($StringLength != 0) { 
    $Location = substr($value,13,$StringLength); 
    ///print "LOCATION:$Location\n";
  }
  $value = substr($value,2); //This puts $value in line with the rest
}
// probably going to end up processing repeat events in if/else crap...
else {
  //Well we have to process the rest, may as well delete some end cruft
  //removing the last 35 is silly, contains where location would be.
  //$value=substr($value,0,-35);

  list(,$ExclusionCount) = unpack("v",substr($value,0,2));
  $value = substr($value,2);
  //echo "ExclusionCount $ExclusionCount\n";
  while ($ExclusionCount != 0) {
    list(,$c) = unpack("V",substr($value,0,4));
    // iCal is anal about EXDATE
    //$midnight = $c % 86400;
    //$stamp = floor($c/86400)*86400;
    //$offset = date("Z",$StartTime)+$midnight;
    //FIXME: I fucked up here, but its apparently working fine
    if (date("I",$t)) {
    // && date("Z",$t) != -25200
    $Offset=date("Z",$t)-"-25200";
    } else {
    $Offset=date("Z",$t)-"-28800";
    }
    $time = ($StartTime+date("Z",$StartTime)) % 86400;
    $stamp = $c + $time - $Offset;
    //print "$ExclusionCount $c $stamp $time \n";
    //."T".xxdate("His",$StartTime)."\n";
    $Exclusions[$ExclusionCount-1] = date("Ymd",$c);
    $xclusions[$ExclusionCount-1]=$stamp;
    //print "EXDATE$TZID:".date("Ymd",$c)."T".xxdate("His",$StartTime)."\n";
    //echo $ExclusionCount." ".$Exclusions[$ExclusionCount-1]."\n";
    $value = substr($value,4);
    $ExclusionCount--;
  }
  //hexdebug($value);
  //Now for repeats!
  if (substr($value,0,6) == "\x01\x80\x03\0\0\0" || substr($value,6,11) == "CDayOfMonth") {
    if (substr($value,0,6) == "\x01\x80\x03\0\0\0") 
    { $value = substr($value,2); } else { $value = substr($value,17); }
    //RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1WE
    //ef a1 9e 74 00 00 00 00 04 00 00 00 01 00 00 00
    //  date     |            TH           2
    //work backwards with this
    list(,$LastDate) = unpack("V",substr($value,8,4));
    list(,$WK) = unpack("v",substr($value,20,2));
    $WK++;
    list(,$WD) = unpack("v",substr($value,16,2));
    $WD=$MOTUWE[$WD];
    $WD=$WK.$WD;
    //TODO location a 1st Weds and check
    $value=substr($value,4); //4 to fudge
    print "RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=".date("Ymd",$LastDate).";BYDAY=$WD\n";
    //print "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=$WD\n";
    //exit;
  }
  elseif (substr($value,0,10) == "\x06\x80\x01\0\0\0\x01\0\0\0" || substr($value,6,8) == "CDayName") {
    if (substr($value,0,10) == "\x06\x80\x01\0\0\0\x01\0\0\0")
    { $value = substr($value,2); } else { $value = substr($value,14); }
    list(,$LastDate) = unpack("V",substr($value,8,4));
    //Seens fine for now, but really shouldn't specify a time for 
    //timeless events
    print "RRULE:FREQ=DAILY;UNTIL=".date("Ymd",$LastDate)."T".gmdate("His",$StartTime)."Z\n";
    $value = substr($value,20); //TODO Check this guess.
  }
  //Weekly
  elseif (substr($value,0,6) == "\x0b\x80\x02\0\0\0" || substr($value,6,7) == "CWeekly") {
    if (substr($value,0,6) == "\x0b\x80\x02\0\0\0") 
    { $value = substr($value,2); } else { $value = substr($value,13); }
    list(,$LastDate) = unpack("V",substr($value,8,4));
    list(,$WK) = unpack("V",substr($value,4,4));
    $WD = ord(substr($value,20,1));
    //$WD = $MOTUWE[$WD];
    $WD = BitmaskWeekdays($WD);
    $value = substr($value,21);
    //echo "$LastDate ".date("Ymd",$LastDate)." $WK $WD\n";
    //RRULE:W3 WE 20311231
    echo "RRULE:FREQ=WEEKLY;UNTIL=".date("Ymd",$LastDate).";INTERVAL=$WK;BYDAY=$WD\n";
  }
  //Unknown, so just dump out raw info to look at
  else {
    echo "$value\n";
    echo "RRULE:".chunk_split(implode(" ",unpack("H*",$value)),2," ") . "\n";
  }
  //hexdebug($value);
  //location
  $StringLength = ord(substr($value,8,1));
  if ($StringLength != 0) { $Location = substr($value,9,$StringLength); }
} // else thats not indented, just after if no repeat -> check location bit

//print "Item $Count, C$Category $Title\n";
if ($StartTime != $EndTime) {
echo "DTSTART;TZID=$TZID:" . xxdate("Ymd",$StartTime)."T".xxdate("His",$StartTime)."\n";
echo "DTEND;TZID=$TZID:" . xxdate("Ymd",$EndTime)."T".xxdate("His",$EndTime)."\n";
} else {
echo "DTSTART;TZID=$TZID:" . xxdate("Ymd",$StartTime)."\n";
echo "DTEND;TZID=$TZID:" . xxdate("Ymd",$EndTime)."\n";
}
if (isset($Location)) {
  print "LOCATION:$Location\n";
}

if (isset($Exclusions)) {
  print "EXDATE:" . join(",",$Exclusions)."\n";
  foreach ($xclusions as $c) {
    print "EXDATE;TZID=$TZID:".xxdate("Ymd",$c)."T".xxdate("His",$c)."\n";
  }
}
      
//echo $StringLength;
//echo $StartTime ." " . date("F jS, Y @ g:i:sa",$StartTime);
//print "<br>\n";
//echo $EndTime ." " . date("F jS, Y @ g:i:sa",$EndTime);
//print "<br>\n";
if (isset($Note)) { 
$Note = str_replace(",","\,",$Note);
print "DESCRIPTION:".str_replace("\r\n","\\n",$Note)."\n"; 
}
/*if (substr($value,0,12) != "\0\0\0\0\x05\0\0\0\0\0\0\0") {
print "<pre>";
echo $value."\n";
echo chunk_split(implode(" ",unpack("H*",$value)),2," ") . "<br>\n";
echo md5($value)."\n";
print "</pre>";
}*/
print "END:VEVENT\n";
}
//print $Entries[0]; print $DatabookFile;

//$Birthdays=true;
if ($Birthdays && file_exists("birthdays.txt")) {
  //include("address.php");
  print file_get_contents("birthdays.txt");
}
print "END:VCALENDAR\n";

?>
